''' <summary>
''' This module was created to provide direct read and write access to Paradox tables, thus bypassing the very slow Paradox access inherent in ADO.Net
''' The class ignores all indices; when reading from a table the records are processed sequentially and the indices are ignored;
''' When writing to a table, it must already exist and must contain no indices. The performance improvement is remarkable; what used to take 15 minutes not takes only seconds
''' </summary>
''' <remarks></remarks>
Friend Module modParadox

    'Following taken from text file "PxFormat.txt"...
    'Corrections to this description are taken from file "Paradox4.txt" by Kevin Mitchell and shown in brackets


    ' The PARADOX File Structure                         Compiled by Randy Beck
    ' ==========================                               rb@randybeck.com
    '                                                  http://www.randybeck.com

    '    I haven't made changes to this description for a long while, but not
    '    much has changed.  The latest revision concerns the tableName field,
    '    which I've discovered has lengthened in Paradox 7, and then another
    '    revision to that same field yet again.

    '    I'm not aware of any changes after Paradox 7.  But if anybody knows of
    '    anything different in Paradox 8 and 9 then please let me know.

    '    I should say here that many of the fields are listed here as pointers
    '    while the file is in memory.  I first made this connection back when
    '    Paradox 4 was still the state of the art.  I have not verified that
    '    this remains true, but a quick glance at some of the fields makes me
    '    think it's probably still true.


    ' This document details the binary file format for Paradox data files.
    ' There are still a few unknowns, but the important items are covered.

    ' I refer to the older table format as 3.0 tables and the newer ones as 4.0
    ' through 7.0, but I believe that most of the 3.0 information may also apply
    ' to the earlier versions.

    ' Pascal terms are used to describe data types:
    '     byte is 1 byte unsigned;
    '     integer is a 2-byte signed integer;
    '     word is a 2-byte unsigned integer;
    '     longint is a 4-byte signed integer;
    '     char is a 1-byte character;
    '     pchar is a pointer to a character;
    '     ^ modifies any type definition to a pointer to that type;
    '     ^pchar is a pointer to a pointer to a character.

    ' All pointers are 4-byte pointers.

    ' Please send additions and corrections to:

    '     Randy Beck
    '     P.O. Box 530433
    '     DeBary, FL 32753-0433
    '     USA

    '     email: rb@randybeck.com

    ' Distribute this freely, but please leave my name and address
    ' intact so that others may add information.




    ' GENERAL FILE STRUCTURE
    ' ======================

    '    ============================
    '    |     Header               |
    '    |     Data Block 0         |
    '    |     Data Block 1         |
    '    |     Data Block 2         |
    '    |     ...                  |
    '    |     Data Block n         |
    '    ============================

    ' The size of the Paradox file header is usually 2048 bytes (see headerSize
    ' at file offset 0002).  The first portion (offsets 0000 to $0057) has fixed
    ' field locations.  The next section ($0058 to $0077) was new to Paradox 4.0
    ' data files.  The rest varies in size -- depending upon the upon the number
    ' of fields -- and is listed sequentially.

    ' Some of the information in the file header seems to be needed only while
    ' it is being used by Paradox in RAM.  A few of these fields are pointers to
    ' other fields in the header, and are valid only during run-time.  They are
    ' listed here anyway, although their meaning is subject to change and they
    ' serve little purpose for third-party software.

    ' The data area which follows is divided into record blocks.  These use 1024,
    ' 2048, 3072 or 4096 bytes each, depending upon the maximum table size set
    ' when the table was created.  The field structure of the data area is itself
    ' unusual in that each data field is arranged in hi-byte to low-byte order.

    ' Note:  This hi-byte to low-byte arrangement only applies to the user data
    ' in the table.  Everything else uses the normal low-byte to hi-byte format.

    ' The structure of primary and secondary index files generally follows that
    ' of Paradox version 3 data files.




    ' Paradox Common File Header  -- offsets 0000 to 0057
    ' ==========================

    ' With some noted exceptions, this part of the description is common to data
    ' and index files.  Offsets in this list are given in hexadecimal.  Other
    ' numbers should be assumed as listed in decimal format unless preceded by a
    ' dollar sign ('$').

    ' offset  type        usage
    '==============================================================================
    '| 0000 | integer     recordSize                                              |
    '|      |                                                                     |
    '|      |        This is the size of a user record in this table.             |
    '|      |                                                                     |
    '|      |        For primary index files, each "record" is actually the       |
    '|      |        field or fields in the index, plus three integers which      |
    '|      |        are not referenced in the header.                            |
    '|      |                                                                     |
    '|      |        Secondary index files also have additional fields, but       |
    '|      |        these are listed in the header.                              |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0002 | integer     headerSize (always $0800)                               |
    '|      |                                                                     |
    '|      |        You can change headerSize, and move the data blocks          |
    '|      |        accordingly, to create larger or smaller table headers.      |
    '|      |        Borland's TUTILITY program would flag an error, but          |
    '|      |        Paradox, the Borland Database Engine and the Paradox         |
    '|      |        Engine will all still work with these tables.                |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0004 | byte        fileType                                                |
    '|      |                                                                     |
    '|      |           0 = this is an indexed .DB data file                      |
    '|      |           1 = this is a primary index .PX file                      |
    '|      |           2 = this is a non-indexed .DB data file                   |
    '|      |           3 = this is a non-incrementing secondary index .Xnn file  |
    '|      |           4 = this is a secondary index .Ynn file (inc or non-inc)  |
    '|      |           5 = this is an incrementing secondary index .Xnn file     |
    '|      |           6 = this is a non-incrementing secondary index .XGn file  |
    '|      |           7 = this is a secondary index .YGn file (inc or non inc)  |
    '|      |           8 = this is an incrementing secondary index .XGn file     |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0005 | byte        maxTableSize                                            |
    '|      |                                                                     |
    '|      |        This is the "maximum table size" determined when this        |
    '|      |        table was created.  It really indicates the size of each     |
    '|      |        block of records in the data section of the table.           |
    '|      |                                                                     |
    '|      |           1 =   64M    (block size = $0400 bytes)                   |
    '|      |           2 =  128M    (block size = $0800 bytes)                   |
    '|      |           3 =  192M    (block size = $0C00 bytes)                   |
    '|      |           4 =  256M    (block size = $1000 bytes)                   |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0006 | longint     numRecords                                              |
    '|      |                                                                     |
    '|      |        This is the number of records in this file.                  |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 000A | word        nextBlock [NUMBER OF BLOCKS IN USE]                     |
    '|      |                                                                     |
    '|      |        I'm not certain what this really is, but it seems to be      |
    '|      |        the same as fileBlocks unless there is an empty block in     |
    '|      |        the table.                                                   |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 000C | word        fileBlocks                                              |
    '|      |                                                                     |
    '|      |        This is the number of data blocks in the file.               |
    '|      |        (Each "block" is a cluster of records.)                      |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 000E | word        firstBlock                                              |
    '|      |                                                                     |
    '|      |        Always 1 unless the table is empty.                          |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0010 | word        lastBlock                                               |
    '|      |                                                                     |
    '|      |        This works out to the number of blocks that the table        |
    '|      |        would contain if every block was packed.                     |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0012 | word        unknown                                                 |
    '|      |                                                                     |
    '|      |        The value of this field seems to the change when records     |
    '|      |        or blocks have been added to the table, but I still haven't  |
    '|      |        figured it out.                                              |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0014 | byte        modifiedFlags1                                          |
    '|      |                                                                     |
    '|      |        A rebuild is required if this is not zero.                   |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0015 | byte        indexFieldNumber                                        |
    '|      |                                                                     |
    '|      |        In the .Xnn file of a secondary index, this is the number    |
    '|      |        of the field it is referencing.                              |
    '|      |                                                                     |
    '|      |        This will be zero in the other files.                        |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0016 | pointer     primaryIndexWorkspace                                   |
    '|      |                                                                     |
    '|      |        Pointer to the primary index file header (in RAM).           |
    '|      |        This will be a NIL if there is no primary index.             |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 001A | pointer     unknown  (suspected pointer)                            |
    '|      |                                                                     |
    '|      |        This field is usually a NIL pointer.  I've only seen it      |
    '|      |        used in 5.0 tables with BCD field types.  It is probably     |
    '|      |        just a workspace pointer.                                    |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 001E...0020        unknown                                                 |
    '|      |                                                                     |
    '|      |        I have only seen these three bytes used in .PX files.        |
    '|      |                                                                     |


    '+------+---------------------------------------------------------------------+
    '| 0021 | integer     numFields                                               |
    '|      |                                                                     |
    '|      |        This is the number of fields in the table.  If this is an    |
    '|      |        index file, then it would only be the number of fields in    |
    '|      |        this index.                                                  |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0023 | integer     primaryKeyFields                                        |
    '|      |                                                                     |
    '|      |        This is the number of fields in the file's primary key.      |
    '|      |        It will be a zero for .PX and .Ynn files; and 2 for .Xnn     |
    '|      |        secondary index files.                                       |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0025 | longint     encryption1                                             |
    '|      |                                                                     |
    '|      |        This was where the encryption information was stored for     |
    '|      |        versions 3.0 and 3.5.  (It was a zero if not encrypted.)     |
    '|      |                                                                     |
    '|      |        Subsequent versions store the value $FF00FF00 here, and      |
    '|      |        move the encryption code to offset $005C.  Even so, these    |
    '|      |        newer versions still maintain this information at both       |
    '|      |        locations while working in RAM.                              |
    '|      |                                                                     |
    '|      |        Primary and .Ynn secondary index files always use this       |
    '|      |        field to store the encryption code, but it is often a        |
    '|      |        zero because the Paradox Engine and the Borland Database     |
    '|      |        Engine do not always encrypt index files.  You can encrypt   |
    '|      |        unencrypted index files by following these steps:            |
    '|      |                                                                     |
    '|      |           Begin with an empty encrypted table;                      |
    '|      |           Truncate the index files to headerSize;                   |
    '|      |           Zeroize nextBlock, fileBlocks, firstBlock and lastBlock;  |
    '|      |           Copy four bytes from the .DB data file's encryption1      |
    '|      |               field (for version 3), or the encryption2 field       |
    '|      |               (for versions 4 and above) into the encryption1       |
    '|      |               or encryption2 field of the index file;               |
    '|      |           Test thoroughly.                                          |
    '|      |                                                                     |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0029 | byte        sortOrder                                               |
    '|      |                                                                     |
    '|      |           $00:  ASCII                                               |
    '|      |           $B7:  International                                       |
    '|      |           $82:  Norwegian/Danish                                    |
    '|      |           $E6:  Norwegian/Danish (4.0)                              |
    '|      |           $F0:  Swedish/Finnish                                     |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 002A | byte        modifiedFlags2                                          |
    '|      |                                                                     |
    '|      |        A rebuild is required if this is not zero.                   |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 002B...002C        unknown    (always 0)                                   |
    '+------+---------------------------------------------------------------------+
    '| 002D | byte        changeCount1                                            |
    '|      |                                                                     |
    '|      |        This is incremented whenever the file header is updated.     |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 002E | byte        changeCount2                                            |
    '|      |                                                                     |
    '|      |        I'm not certain when this is incremented.                    |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 002F | byte        unknown                                                 |
    '+------+---------------------------------------------------------------------+
    '| 0030 | ^pchar      tableNamePtrPtr                                         |
    '|      |                                                                     |
    '|      |        This is a pointer to tableNamePtr, which is a pointer to     |
    '|      |        tableName.  Paradox uses this field to gain faster access    |
    '|      |        to tableName because that part of the header is accessed     |
    '|      |        sequentially.                                                |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0034 | pointer     fldInfoPtr                                              |
    '|      |                                                                     |
    '|      |        Pointer to the list of field identifiers.  This is listed    |
    '|      |        in the accompanying Pascal record definition as a            |
    '|      |        PFldInfoRec type.                                            |
    '|      |                                                                     |
    '|      |        You can use this pointer value to locate the table header    |
    '|      |        in memory during run time.  Just subtract $0078 from this    |
    '|      |        value (for 4.0+ tables), or $0058 (for .PX and .Ynn index    |
    '|      |        files and version 3.0 tables).                               |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0038 | byte        writeProtected                                          |
    '|      |                                                                     |
    '|      |           0        write protection OFF                             |
    '|      |           1        write protection ON                              |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0039 | byte        fileVersionID                                           |
    '|      |                                                                     |
    '|      |           $03      version 3.0                                      |
    '|      |           $04      version 3.5                                      |
    '|      |           $05..09  version 4.x   (usually = $09)                    |
    '|      |           $0A,$0B  version 5.x                                      |
    '|      |           $0C      version 7.x                                      |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 003A | word        maxBlocks                                               |
    '|      |                                                                     |
    '|      |        I don't know what this is for.  It is usually the same       |
    '|      |        as fileBlocks (at offset 000C).                              |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 003C | byte        unknown                                                 |
    '+------+---------------------------------------------------------------------+
    '| 003D | byte        auxPasswords                                            |
    '|      |                                                                     |
    '|      |        Number of auxiliary passwords assigned to the table.         |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 003E...003F        unknown                                                 |
    '+------+---------------------------------------------------------------------+
    '| 0040 | pointer     cryptInfoStartPtr                                       |
    '|      |                                                                     |
    '|      |        Points to cryptInfo field.  It is always NIL when not        |
    '|      |        encrypted.  It is sometimes NIL even when encrypted.         |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0044 | pointer     cryptInfoEndPtr                                         |
    '|      |                                                                     |
    '|      |        Points to end of cryptInfo.  This is NIL if not encrypted.   |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0048 | byte        unknown                                                 |
    '+------+---------------------------------------------------------------------+
    '| 0049 | longint     autoInc                                                 |
    '|      |                                                                     |
    '|      |        This long integer stores the value used for the next auto    |
    '|      |        incrementing field in tables with a "+" autoincrementing     |
    '|      |        field type.                                                  |
    '|      |                                                                     |
    '|      |        Formerly used as a modification count.                       |
    '|      |                                                                     |
    '|      |        My thanks to Orlando Ruiz for informing me of this change.   |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 004D...004E        unknown [4D IS BLOCK NUMBER OF FIRST FREE BLOCK]        |
    '+------+---------------------------------------------------------------------+
    '| 004F | byte        indexUpdateRequired                                     |
    '+------+---------------------------------------------------------------------+
    '| 0050...0054        unknown                                                 |
    '+------+---------------------------------------------------------------------+
    '| 0055 | byte        refIntegrity                                            |
    '|      |                                                                     |
    '|      |        A value here (=2?) denotes that this table uses              |
    '|      |        referential integrity checks.                                |
    '|      |                                                                     |
    '|      |                                                                     |
    '|      |             inxDirection (sec'y index file only)                    |
    '|      |                                                                     |
    '|      |        Secondary .Xnn index files of v7.0 tables use this           |
    '|      |        field to indicate sort order direction:                      |
    '|      |                                                                     |
    '|      |           $01      ascending sort                                   |
    '|      |           $11      descending sort                                  |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0056...0057        unknown                                                 |
    '==============================================================================





    ' Paradox 4+ Data File Header -- offsets 0058 to 0077
    ' ===========================

    ' This part of the description applies only to .DB data files and .Xnn index
    ' files for Paradox versions 4.0 and later.

    ' offset  type        usage
    '==============================================================================
    '| 0058 | integer     unknown  (file version ID?)                             |
    '|      |                                                                     |
    '|      |           $0105..$0109  version 4.x   (usually = $0109)             |
    '|      |           $010A, $010B  version 5.x   (usually = $010B)             |
    '|      |           $010C         version 7.0                                 |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 005A | integer     unknown  (file version ID?)                             |
    '|      |                                                                     |
    '|      |        same values as at 0058                                       |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 005C | longint     encryption2                                             |
    '|      |                                                                     |
    '|      |        This will be zero if not encrypted.                          |
    '|      |        See encryption1 at offset 0025.                              |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0060 | longint     fileUpdateTime (4.x only)                               |
    '|      |                                                                     |
    '|      |        Format similar to a packed date and time.                    |
    '|      |        I don't know what this does in 5.0 tables.                   |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0064 | integer     hiFieldID                                               |
    '|      |                                                                     |
    '|      |        This number is always numFields + 1.                         |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0066 | integer     hiFieldIDinfo?                                          |
    '|      |                                                                     |
    '|      |        This is related to hiFieldID (above), but I don't really     |
    '|      |        know what it's for.                                          |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0068 | integer     sometimesNumFields?                                     |
    '|      |                                                                     |
    '|      |        This is sometimes the number of fields in the table, but     |
    '|      |        is often just a zero.  I don't know why.                     |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 006A | integer     dosGlobalCodePage                                       |
    '|      |                                                                     |
    '|      |        This was the Global Code Page when this table was created.   |
    '|      |                                                                     |
    '|      |           $01B5   United States                                     |
    '|      |           $02E1   Greek 1                                           |
    '|      |           $0352   Multilingual (Latin I)                            |
    '|      |           $0354   Eastern European (Latin II)                       |
    '|      |           $0359   Turkish                                           |
    '|      |           $035C   Portuguese                                        |
    '|      |           $035D   Icelandic                                         |
    '|      |           $035F   Canadian French                                   |
    '|      |           $0361   Nordic                                            |
    '|      |           $0365   Greek 2                                           |
    '|      |                                                                     |
    '|      |        Refer to an MS-DOS technical reference about interrupt $21,  |
    '|      |        function $66, for more information about this.               |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 006C...006F        unknown                                                 |
    '+------+---------------------------------------------------------------------+
    '| 0070 | integer     changeCount4                                            |
    '+------+---------------------------------------------------------------------+
    '| 0072...0077        unknown                                                 |
    '==============================================================================





    ' Paradox Common File Header  -- continued
    ' ==========================

    ' The file header continues sequentially.  Since the number of fields varies,
    ' there are no subsequent fixed offsets.

    ' This section begins where the previous section left off:

    ' offset
    '==============================================================================
    '| 0058   Paradox tables version 3.0 and 3.5                                  |
    '| 0058   Paradox .PX and .Ynn index files (any listed version)               |
    '| 0078   Paradox tables version 4.0 and above                                |
    '| 0078   Paradox .Xnn index files version 4.0 and above                      |
    '==============================================================================

    '         type                                   usage
    '==============================================================================
    '| ---- | array[1..(numFields)] of TFldInfoRec   fieldInfo                    |
    '|      |                                                                     |
    '|      |        type  TFldInfoRec  = RECORD                                  |
    '|      |                  fType   : byte;                                    |
    '|      |                  fSize   : byte;                                    |
    '|      |              end;                                                   |
    '|      |                                                                     |
    '|      |        These are the field identifiers for each field in the        |
    '|      |        table:                                                       |
    '|      |                                                                     |
    '|      |           fType  fSize(decimal)                                     |
    '|      |           -------------------------                                 |
    '|      |            $01     v   "A"  Alpha                                   |
    '|      |            $02     4   "D"  Date                                    |
    '|      |            $03     2   "S"  Short integer                           |
    '|      |            $04     4   "I"  Long integer                            |
    '|      |            $05     8   "$"  currency                                |
    '|      |            $06     8   "N"  Number                                  |
    '|      |            $09     1   "L"  Logical                                 |
    '|      |            $0C     v   "M"  Memo BLOb                               |
    '|      |            $0D     v   "B"  Binary Large Object                     |
    '|      |            $0E     v   "F"  Formatted Memo BLOb                     |
    '|      |            $0F     v   "O"  OLE                                     |
    '|      |            $10     v   "G"  Graphic BLOb                            |
    '|      |            $14     4   "T"  Time                                    |
    '|      |            $15     8   "@"  Timestamp                               |
    '|      |            $16     4   "+"  Autoincrement                           |
    '|      |            $17    17*  "#"  BCD                                     |
    '|      |            $18     v   "Y"  Bytes                                   |
    '|      |                                                                     |
    '|      |        The fSize given for BCD fields is not used for field size.   |
    '|      |        Instead, fSize denotes the number of digits following the    |
    '|      |        decimal point.  BCD fields are always 17 bytes long.         |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| ---- | pchar                                  tableNamePtr                 |
    '|      |                                                                     |
    '|      |        Pointer to tableName when header is in RAM.                  |
    '|      |                                                                     |
    '+======+=====================================================================+
    '| ---- | array[1..(numFields)] of pchar         fieldNamePtrArray            |
    '|      |                                                                     |
    '|      |      * These pointers are not present in the index files. *         |
    '|      |                                                                     |
    '|      |        This is an array of pointers that reference the field names  |
    '|      |        when Paradox (or one of the engines) is running.  The size   |
    '|      |        of this array depends upon the number of fields.             |
    '|      |                                                                     |
    '+======+=====================================================================+
    '| ---- | array[1..(length varies)] of char      tableName                    |
    '|      |                                                                     |
    '|      |        This was the name this file was assigned when created.       |
    '|      |                                                                     |
    '|      |        Most tables will use 79 bytes, padded with zeroes.           |
    '|      |        This was extended with Paradox 7 to 261 bytes.               |
    '|      |                                                                     |
    '|      |        NOTE:  It was previously listed here as 271 bytes but        |
    '|      |               I received a heads-up from George Thackray that       |
    '|      |               this was in error.  It may also be that there's       |
    '|      |               there are more variations.                            |
    '|      |                                                                     |
    '==============================================================================
    '|                                                                            |
    '|     The rest of this information does not apply to .PX and .Ynn files.     |
    '|                                                                            |
    '==============================================================================
    '| ---- | char[]                                 fieldNames                   |
    '|      |                                                                     |
    '|      |        These are the ASCIIZ field name(s), arranged sequentially.   |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| ---- | record                                 cryptInfo                    |
    '|      |                                                                     |
    '|      |        Encrypted tables would have additional data here.            |
    '|      |                                                                     |
    '|      |        Tables with auxiliary passwords would have 256 bytes here    |
    '|      |        (about which I have no information).                         |
    '|      |        Encrypted tables without auxiliary passwords would have      |
    '|      |        one byte for each field here.                                |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| ---- | array[1..(numFields)] of integer       fieldNumbers                 |
    '|      |                                                                     |
    '|      |        These seem to be field numbers.  Changing these numbers      |
    '|      |        in my limited experiments seemed to cause no change in       |
    '|      |        behavior.  TUTILITY didn't seem to mind either.              |
    '|      |                                                                     |
    '|      |        I define this as an array, but the size is determined by     |
    '|      |        the number of fields in the table.                           |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| ---- | char[]      sortOrderID                                             |
    '|      |                                                                     |
    '|      |        An ASCIIZ string representing the sort order for this        |
    '|      |        table ("ascii", "intl", etc.).                               |
    '|      |                                                                     |
    '==============================================================================






    ' Paradox Data Blocks
    ' ===================

    ' The data area begins at offset headerSize (usually $0800).  It is divided
    ' into blocks of 1024, 2048, 3072 or 4096 bytes -- depending upon the maximum
    ' table size set when the table was created.

    ' The entire data block will be encrypted if the table is encrypted.

    '  byte   type      usage
    '==============================================================================
    '| 0000 | word      nextBlock (block number + ???)                            |
    '|      |        [NEXT BLOCK NUMBER (0 IF LAST BLOCK)]                        |
    '|      |        I don't know what this does.                                 |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0002 | word      blockNumber                                               |
    '|      |        [PREVIOUS BLOCK NUMBER (0 IF FIRST)]                         |
    '|      |        The first block is numbered zero.                            |
    '|      |                                                                     |
    '+------+---------------------------------------------------------------------+
    '| 0004 | integer   addDataSize                                               |
    '|      |        [OFFSET IN BYTES TO LAST RECORD IN BLOCK (same thing)        |
    '|      |        This represents the amount of data in this block -- in       |
    '|      |        addition to one record length.                               |
    '|      |                                                                     |
    '|      |        This will be a zero if there is one record in this block,    |
    '|      |        and a negative number if there are no records.               |
    '|      |                                                                     |
    '|      |           numRecsInBlock = (addDataSize / recordSize) + 1           |
    '|      |                                                                     |
    '==============================================================================
    '| 0006.......      fileData                                                  |
    '|                                                                            |
    '|               Block size varies according to maxTableSize (at 0005):       |
    '|                  maxTableSize =  1  (64M):   block size = $0400 bytes      |
    '|                  maxTableSize =  2 (128M):   block size = $0800 bytes      |
    '|                  maxTableSize =  3 (192M):   block size = $0C00 bytes      |
    '|                  maxTableSize =  4 (256M):   block size = $1000 bytes      |
    '|                                                                            |
    '|                                                                            |
    '|   The records in the data area are arranged in field order.  Alpha fields  |
    '|   are arrays of characters, padded with zeroes.  Other fields seem to be   |
    '|   arranged in hi-byte to low-byte order, with the first byte's high bit    |
    '|   set for positive numbers.                                                |
    '|                                                                            |
    '|   For example, the number 1 stored as a 2-byte Short integer field is      |
    '|   arranged this way:  $80, 01.  And the integer 256 would be stored as:    |
    '|   $81, 00.                                                                 |
    '|                                                                            |
    '|   Floating point Number and currency fields are 8-byte DOUBLE types,       |
    '|   hi-byte to low-byte, with the first byte's high bit set.                 |
    '|                                                                            |
    '|   Date fields are stored as the number of days since JAN-0-0000, in high-  |
    '|   byte to low-byte order with the first byte's high bit set.               |
    '|                                                                            |
    '|                                                                            |
    '|   If you are wondering why the first byte's high bit is set for positive   |
    '|   numbers, recall from the Paradox Engine's documentation that a "blank"   |
    '|   SHORT (2-byte) integer is assigned the value of $8000.  By using the     |
    '|   hi-byte to low-byte format, and by reversing the first byte's high bit,  |
    '|   the value $8000 is converted to all zeroes.  So a record stored as all   |
    '|   zeroes is actually all blanks.                                           |
    '|                                                                            |
    '|                                                                            |
    '|   The records in primary index files contain the field or fields in the    |
    '|   index -- plus three integers that are not described in the header's      |
    '|   fieldInfo area.                                                          |
    '|                                                                            |
    '|                                                                            |
    '==============================================================================

    'Paradox codes for file types
    Friend Enum enumFileType As Byte
        IndexedDB
        PrimaryPX
        NonIndexedDB
        SecondaryNonIncrXnn
        SecondaryYnn
        SecondaryIncrXnn
        SecondaryNonIncrXGn
        SecondaryYGn
        SecondaryIncrXGn
    End Enum

    'Paradox codes for block size
    Friend Enum enumBlockSize As Byte
        _1024 = 1
        _2048 = 2
        _3072 = 3 'not used
        _4096 = 4
        _16384 = 16 'check these
        _32768 = 17
    End Enum

    'note that only version 4 and later are supported
    Friend Enum enumFileVersion As Byte
        v3 = &H3
        v35 = &H4
        v4_1 = &H5
        v4_2 = &H6
        v4_3 = &H7
        v4_4 = &H8
        v4_5 = &H9
        v5_1 = &HA
        v5_2 = &HB
        v4 = &H9
        v5 = &HB
        v7 = &HC
    End Enum

    'note that only version 4 and later are supported
    Friend Enum enumFileVersion2 As Int16
        v4 = &H109
        v5 = &H10B
        v7 = &H10C
    End Enum

    'Paradox codes for field types
    Friend Enum enumFieldType As Byte
        pxfAlpha = &H1
        pxfDate = &H2
        pxfShort = &H3
        pxfLong = &H4
        pxfCurrency = &H5
        pxfNumber = &H6
        pxfLogical = &H9
        pxfMemoBLOb = &HC
        pxfBLOb = &HD
        pxfFmtMemoBLOb = &HE
        pxfOLE = &HF
        pxfGraphic = &H10
        pxfTime = &H14
        pxfTimestamp = &H15
        pxfAutoInc = &H16
        pxfBCD = &H17
        pxfBytes = &H18
    End Enum

    ''' <summary>
    ''' field information record used in TPxHeader below
    ''' </summary>
    ''' <remarks></remarks>
    Friend Structure FldInfoRec
        Dim fType As enumFieldType
        Dim fSize As Byte
    End Structure

    ''' <summary>
    ''' This is the static portion of the header (fixed size); it can be accessed in one fell swoop
    ''' </summary>
    ''' <remarks></remarks>
    Friend Structure PDoxHeader
        Dim recordSize As UInt16                'user record size
        Dim headerSize As UInt16                'always &H0800
        Dim fileType As enumFileType            'filetype code
        Dim blockSizeCode As enumBlockSize      'block size code
        Dim numRecords As Int32                 'number of records in file
        Dim nextBlock As UInt16                 'same as fileBlocks?
        Dim fileBlocks As UInt16                'number of data blocks
        Dim firstBlock As UInt16                'always 1 unless empty
        Dim lastBlock As UInt16                 'number of blocks if every one was packed
        Dim unknown12x13 As UInt16
        Dim modifiedFlags1 As Byte              'rebuild reqd if not zero
        Dim indexFieldNumber As Byte            'field number of index if Xnn
        Dim primaryIndexWorkspace As Integer    'pointer to primary index (if RAM)
        Dim unknownPtr1A As Integer
        Dim unknown1Ex1F As Int16
        Dim unknown20 As Byte
        Dim numFields As Int16                  'number of fields in table
        Dim primaryKeyFields As Int16           'number of fields in primary key
        Dim encryption1 As Int32                'zero if not encrypted (v 3.0 & 3.5)
        Dim sortOrder As Byte                   'sort order code (&H00 for ASCII)
        Dim modifiedFlags2 As Byte              'rebuild reqd if not zero
        Dim unknown2Bx2C As Int16               'always 0
        Dim changeCount1 As Byte                'incremented when header is updated
        Dim changeCount2 As Byte                '?
        Dim unknown2F As Byte
        Dim tableNamePtrPtr As Integer          'pointer to tablename pointer
        Dim fldInfoPtr As Integer               'pointer to field names
        Dim writeProtected As Byte              '1 if write protected
        Dim fileVersionID As enumFileVersion    'file version code
        Dim maxBlocks As UInt16                 'same as fileBlocks?
        Dim unknown3C As Byte
        Dim auxPassuint16s As Byte              'number of passwords
        Dim unknown3Ex3F As Int16
        Dim cryptInfoStartPtr As Integer        'encryption pointer
        Dim cryptInfoEndPtr As Integer          'encryption pointer
        Dim unknown48 As Byte
        Dim autoIncVal As Int32                 'value used for next autoinc value
        Dim unknown4Dx4E As Int16
        Dim indexUpdateRequired As Byte
        Dim unknown50x53 As Int32
        Dim unknown54 As Byte
        Dim refIntegrity As Byte                'in non-zero has ref integrity
        Dim unknown56x57 As Int16
        Dim fileVerID2 As Int16 'enumFileVersion2      'file version id?
        Dim fileVerID3 As Int16 'enumFileVersion2       'don't use enum--ReadStruct assumes all enums are byte
        Dim encryption2 As Int32                '0 if not encrypted
        Dim fileUpdateTime As Int32             ' 4.0 only 
        Dim hiFieldID As UInt16                 'always numfields + 1
        Dim hiFieldIDinfo As UInt16             '?
        Dim sometimesNumFields As Int16         '?
        Dim dosCodePage As UInt16               'Global code page &H01B5 for US
        Dim unknown6Cx6F As Int32
        Dim changeCount4 As Int16
        Dim unknown72x73 As Int16
        Dim unknown74x77 As Int32
    End Structure

    ''' <summary>
    ''' This section of the header is variable sized and must be read/written one field at a time
    ''' </summary>
    ''' <remarks></remarks>
    Friend Structure PDoxHeader2 'this structure is not actually used!

        'Above is only the first part of the file header.  The last field
        ' is described as an array of 255 elements, but its size is really
        ' determined by the number of fields in the table.  The actual
        ' table header has more information that follows:

        '1. array of two-byte record with field type and size for each field in table (fldinforec)
        '2. pointer to tablename (when in RAM)
        '3. array pointers to fieldnames for each field in table (when in RAM) (NOT PRESENT IN INDEX FILES)
        '4. table name assigned when created (79 bytes or 261 for version 7 files)
        '5. null-terminated field names arranged sequentially
        '6. additional info if encrypted
        '7. integer field numbers for each field in table
        '8. null-terminated string for sort order of table

        Dim fieldInfo() As FldInfoRec           'one for each field
        Dim tableNamePtr As Integer
        Dim fieldNamePtr() As Integer           'one for each field
        Dim tableName As String                 'initialize to 261 characters
        Dim fieldName() As String               'variable length null-terminated fieldnames, one for each field
        Dim fieldNumber() As Int16              'one for each field
        Dim sortOrderStr As String              'null-terminated string
    End Structure


    ''' <summary>
    ''' data blocks begin at offset headerSize and are 1024, 2048, etc. bytes in size; the first block number is 1
    ''' </summary>
    Friend Structure PDoxDataBlock
        Dim nextBlock As UInt16                 '0 if last block
        Dim prevBlock As UInt16                 '0 if first block
        Dim lastRecOffset As Int16                'offset in bytes to last record in block
        Dim fileData As Byte()
    End Structure

    ''' <summary>
    ''' Used to store record items as they are read from the database file
    ''' </summary>
    Friend mItems As New Generic.List(Of Object)

    Private Function FileFormatIsOK(ByVal Hdr As PDoxHeader) As Boolean
        Return (Hdr.blockSizeCode >= 1) And (Hdr.blockSizeCode <= 4)
    End Function

    Private Function FileIsEncrypted(ByVal Hdr As PDoxHeader) As Boolean
        If (Hdr.fileVersionID <= 4) Or Not ItemIn(Hdr.fileType, 0, 2, 3, 5) Then
            Return Hdr.encryption1 <> 0
        Else
            Return Hdr.encryption2 <> 0
        End If
    End Function

    ''' <summary>
    ''' Initialize header for creating Paradox file without BDE
    ''' </summary>
    ''' <param name="TableName">Filename without path (but including .DB)</param>
    ''' <param name="FieldList">Field descriptions</param>
    ''' <remarks>Values inferred from observation of reading .db file created by Paradox; commented fields were non-zero but can probably be zero</remarks>
    Friend Sub InitializeHeader(TableName As String, FieldList As Generic.List(Of clsParadoxField), ByRef Hdr As PDoxHeader, ByRef Hdr2 As PDoxHeader2)
        With Hdr
            'note: members that default to 0 are not included here
            .blockSizeCode = enumBlockSize._16384
            '.changeCount1 
            .dosCodePage = 850
            .encryption1 = -16711936
            .fileType = enumFileType.NonIndexedDB
            '.fileUpdateTime 
            .fileVerID2 = 268
            .fileVerID3 = 268
            .fileVersionID = enumFileVersion.v7
            .fldInfoPtr = 120
            .headerSize = 2048
            .hiFieldID = 16
            .hiFieldIDinfo = 507
            .numFields = FieldList.Count
            .recordSize = 0 'will be accumulated below
            .sortOrder = 0
            .tableNamePtrPtr = 0 'assume set in memory during runtime
            .unknown12x13 = 3
            .unknown3Ex3F = 3871
            .unknown50x53 = 162304
            .unknown56x57 = 32
            .unknown6Cx6F = 35193089
        End With
        With Hdr2
            ReDim .fieldInfo(FieldList.Count - 1)
            ReDim .fieldName(FieldList.Count - 1)
            ReDim .fieldNamePtr(FieldList.Count - 1)
            ReDim .fieldNumber(FieldList.Count - 1)
            .sortOrderStr = "ascii" & Chr(0)
            .tableName = TableName
            .tableNamePtr = 0 'assume set in memory during runtime
            For i As Integer = 0 To FieldList.Count - 1
                With .fieldInfo(i)
                    .fSize = FieldList(i).fSize
                    .fType = FieldList(i).fType
                    Hdr.recordSize += .fSize
                End With
                .fieldName(i) = FieldList(i).fName
                .fieldNamePtr(i) = 0 'assume set in memory during runtime
                .fieldNumber(i) = i + 1
            Next
        End With
    End Sub

    ''' <summary>
    ''' Read file header into memory
    ''' </summary>
    ''' <param name="br">Binary reader instance</param>
    ''' <param name="Hdr">Static header</param>
    ''' <param name="Hdr2">Variable header</param>
    Friend Function GetHeader(ByRef br As IO.BinaryReader, ByRef Hdr As PDoxHeader, ByRef Hdr2 As PDoxHeader2) As Boolean

        ReadStructure(br, Hdr)

        With Hdr2
            'assign the headers fldInfoPtr field
            Hdr.fldInfoPtr = &H78 'only works for 4.0 and above

            'read the variable length part of the header block

            ReDim .fieldInfo(Hdr.numFields - 1)
            For i As Integer = 0 To Hdr.numFields - 1
                With .fieldInfo(i)
                    .fType = br.ReadByte
                    .fSize = br.ReadByte
                End With
            Next

            .tableNamePtr = br.ReadInt32

            ReDim .fieldNamePtr(Hdr.numFields - 1)
            For i As Integer = 0 To Hdr.numFields - 1
                .fieldNamePtr(i) = br.ReadInt32
            Next

            .tableName = ""
            Do
                .tableName &= br.ReadChar
            Loop Until br.PeekChar = 0

            Do
                br.ReadByte()
            Loop Until br.PeekChar <> 0

            ReDim .fieldName(Hdr.numFields - 1)
            For i As Integer = 0 To Hdr.numFields - 1
                Do
                    Dim s As String = br.ReadChar()
                    .fieldName(i) &= s
                Loop Until br.PeekChar = 0
                br.ReadChar()
            Next

            ReDim .fieldNumber(Hdr.numFields - 1)
            For i As Integer = 0 To Hdr.numFields - 1
                .fieldNumber(i) = br.ReadInt16
            Next

            .sortOrderStr = ""
            Do
                .sortOrderStr &= br.ReadChar
            Loop Until br.PeekChar = 0

        End With
        Return True
    End Function

    Friend Function GetNextRecord(ByRef br As IO.BinaryReader, ByRef Hdr As PDoxHeader, ByVal Hdr2 As PDoxHeader2, ByRef Block As PDoxDataBlock, ByRef BlockNum As Integer) As Boolean
        Static LastBlockNum As Integer
        Static RecNum As Integer
        Static Ptr As Integer
        mItems.Clear()

        With Hdr
            If BlockNum > .fileBlocks Then Return False

            If BlockNum = 0 Or BlockNum <> LastBlockNum Then
                If BlockNum = 0 Then 'always go to first block
                    BlockNum = 1
                Else 'go to next free block
                    BlockNum = Block.nextBlock
                End If
                br.BaseStream.Seek(.headerSize + (BlockNum - 1) * .blockSizeCode * &H400, IO.SeekOrigin.Begin)
                With Block
                    .nextBlock = br.ReadUInt16
                    .prevBlock = br.ReadUInt16
                    .lastRecOffset = br.ReadInt16
                    ReDim .fileData(Hdr.blockSizeCode * 1024 - 6 - 1) ' fileData size varies according to maxTableSize 
                    .fileData = br.ReadBytes(.fileData.Length)
                    Ptr = 0
                    RecNum = 0
                End With
                LastBlockNum = BlockNum
            End If
            For f As Integer = 0 To Hdr.numFields - 1
                mItems.Add(GetField(Hdr, Hdr2, Block, Ptr, f))
            Next
            RecNum += 1
            If RecNum > Block.lastRecOffset / Hdr.recordSize Then BlockNum += 1 'so next time will force read of next block
            Return True
        End With
    End Function

    Friend Function GetField(ByRef Hdr As PDoxHeader, ByRef Hdr2 As PDoxHeader2, ByRef Block As PDoxDataBlock, ByRef ptr As Integer, ByVal f As Integer) As Object
        With Hdr2.fieldInfo(f)
            Dim size As Integer = .fSize
            If .fType = enumFieldType.pxfBCD Then size = 17

            Dim ar(size - 1) As Byte
            Buffer.BlockCopy(Block.fileData, ptr, ar, 0, size)

            Dim IsNull As Boolean = True
            For i As Integer = 0 To size - 1
                If ar(i) <> 0 Then IsNull = False : Exit For
            Next
            Dim IsNeg As Boolean = False
            Select Case .fType
                Case enumFieldType.pxfDate To enumFieldType.pxfNumber, enumFieldType.pxfTime To enumFieldType.pxfAutoInc, enumFieldType.pxfLogical
                    If Not IsNull Then
                        If .fType = enumFieldType.pxfNumber Then
                            IsNeg = (ar(0) And &H80) <> &H80 'high bit is set for positive numbers
                            If IsNeg Then
                                For i As Integer = 0 To size - 1
                                    ar(i) = ar(i) Xor &HFF
                                Next
                            End If
                        Else
                            ar(0) = ar(0) Xor &H80
                        End If
                        Array.Reverse(ar)
                    End If
            End Select
            ptr += size

            If IsNull Then Return DBNull.Value

            Select Case .fType
                Case enumFieldType.pxfAlpha, enumFieldType.pxfMemoBLOb
                    Return System.Text.Encoding.ASCII.GetString(ar).TrimEnd(Chr(0))
                Case enumFieldType.pxfShort
                    Return BitConverter.ToInt16(ar, 0)
                Case enumFieldType.pxfLong, enumFieldType.pxfAutoInc
                    Return BitConverter.ToInt32(ar, 0)
                Case enumFieldType.pxfCurrency
                    Return BitConverter.ToDouble(ar, 0)
                Case enumFieldType.pxfNumber
                    Dim dbl As Double = BitConverter.ToDouble(ar, 0)
                    If Not IsNeg Then dbl *= -1
                    Return Math.Round(dbl, 5)
                    'Return Math.Round(BitConverter.ToDouble(ar, 0) * IIf(IsNeg, 1, -1), 5)
                Case enumFieldType.pxfDate, enumFieldType.pxfTimestamp
                    'paradox timestamps are the number of milliseconds since 1/1/0000 (but by inspection appears to actually be one day before 1/1/1)
                    Dim dt As Date = New Date(1, 1, 1)
                    If IsNull Then dt = CDate("1/1/1900") Else dt = dt.AddMilliseconds(BitConverter.ToDouble(ar, 0)).AddDays(-1)
                    Return dt
                Case enumFieldType.pxfLogical
                    Return BitConverter.ToBoolean(ar, 0)
                Case Else
                    Return "UNKNOWN"
            End Select
        End With
    End Function

    Friend Function PutHeader(ByRef bw As IO.BinaryWriter, ByRef Hdr As PDoxHeader, ByRef Hdr2 As PDoxHeader2) As Boolean
        bw.Seek(0, IO.SeekOrigin.Begin)
        WriteStructure(bw, Hdr)

        With Hdr2

            'read the variable length part of the header block

            For i As Integer = 0 To Hdr.numFields - 1
                With .fieldInfo(i)
                    bw.Write(.fType)
                    bw.Write(.fSize)
                End With
            Next

            bw.Write(.tableNamePtr)

            For i As Integer = 0 To Hdr.numFields - 1
                bw.Write(.fieldNamePtr(i))
            Next

            For i As Integer = 0 To 260
                If i <= .tableName.Length - 1 Then
                    bw.Write(CType(Asc(.tableName(i)), Byte))
                Else
                    bw.Write(CType(0, Byte))
                End If
            Next

            For i As Integer = 0 To Hdr.numFields - 1
                For j As Integer = 0 To .fieldName(i).Length - 1
                    bw.Write(CType(Asc(.fieldName(i)(j)), Byte))
                Next
                bw.Write(CType(0, Byte))
            Next

            For i As Integer = 0 To Hdr.numFields - 1
                bw.Write(.fieldNumber(i))
            Next

            For i As Integer = 0 To .sortOrderStr.Length - 1
                bw.Write(CType(Asc(.sortOrderStr(i)), Byte))
            Next
            bw.Write(CType(0, Byte))
        End With
        Return True
    End Function

    Friend Function PutNextRecord(ByRef bw As IO.BinaryWriter, ByRef Hdr As PDoxHeader, ByRef Hdr2 As PDoxHeader2, ByRef Block As PDoxDataBlock, ByRef BlockNum As Integer, ByVal Fields() As String) As Boolean
        Static RecNum As Integer
        Static Ptr As Integer

        With Hdr
            If BlockNum = 0 Or (RecNum + 1) * .recordSize > .blockSizeCode * &H400 - 6 Then 'write block to disk
                If .numRecords > 0 Then WriteBlock(bw, Hdr, Block, BlockNum, False)
                BlockNum += 1
                ReDim Block.fileData(Hdr.blockSizeCode * 1024 - 6 - 1)
                Ptr = 0
                RecNum = 0
            End If
            Block.lastRecOffset = RecNum * .recordSize
            For f As Integer = 0 To Hdr.numFields - 1
                If Not PutField(Hdr, Hdr2, Block, Ptr, f, Fields(f)) Then Return False
            Next
            RecNum += 1
            .numRecords += 1
            Return True
        End With
    End Function

    Friend Sub WriteBlock(ByRef bw As IO.BinaryWriter, ByRef Hdr As PDoxHeader, ByRef Block As PDoxDataBlock, ByVal BlockNum As Integer, ByVal IsLastBlock As Boolean)
        With Hdr
            bw.BaseStream.Seek(.headerSize + (BlockNum - 1) * .blockSizeCode * &H400, IO.SeekOrigin.Begin)
            With Block
                .nextBlock = IIf(IsLastBlock, 0, BlockNum + 1)
                .prevBlock = BlockNum - 1
                bw.Write(.nextBlock)
                bw.Write(.prevBlock)
                bw.Write(.lastRecOffset)
                bw.Write(.fileData)
            End With
            .fileBlocks = BlockNum
            .firstBlock = 1
            .nextBlock = BlockNum
            .lastBlock = BlockNum
            .maxBlocks = BlockNum
        End With
    End Sub

    Friend Function PutField(ByRef Hdr As PDoxHeader, ByRef Hdr2 As PDoxHeader2, ByRef Block As PDoxDataBlock, ByRef ptr As Integer, ByVal f As Integer, ByVal Value As String) As Boolean
        With Hdr2.fieldInfo(f)
            Dim size As Integer = .fSize
            If .fType = enumFieldType.pxfBCD Then size = 17

            Dim ar(size - 1) As Byte
            Dim IsNeg As Boolean = False

            If Not String.IsNullOrEmpty(Value) Then
                Select Case .fType
                    Case enumFieldType.pxfAlpha, enumFieldType.pxfMemoBLOb
                        For i As Integer = 0 To size - 1
                            If i <= CType(Value, String).Length - 1 Then
                                ar(i) = Asc(CType(Value, String)(i))
                            Else
                                ar(i) = 0
                            End If
                        Next
                    Case enumFieldType.pxfShort
                        ar = BitConverter.GetBytes(CType(Val(Value), Int16))
                    Case enumFieldType.pxfLong, enumFieldType.pxfAutoInc
                        ar = BitConverter.GetBytes(CType(Val(Value), Int32))
                    Case enumFieldType.pxfCurrency, enumFieldType.pxfNumber
                        IsNeg = Val(Value) < 0
                        ar = BitConverter.GetBytes(CType(Math.Abs(Val(Value)), Double))
                    Case enumFieldType.pxfLogical
                        If TypeOf Value Is String Then
                            If Value.ToString.ToUpper = "T" Or Value = "1" Then Value = "True"
                            If Value.ToString.ToUpper = "F" Or Value = "0" Then Value = "False"
                            If Value <> "True" And Value <> "False" Then Value = "False"
                        End If
                        ar = BitConverter.GetBytes(CType(Value, Boolean))
                    Case enumFieldType.pxfDate, enumFieldType.pxfTimestamp
                        'paradox timestamps are the number of milliseconds since 1/1/0000 (but by inspection appears to actually be one day before 1/1/1)
                        Dim dt As Date = New Date(1, 1, 1)
                        If Not IsDate(Value) Then Value = "1/1/1900"
                        ar = BitConverter.GetBytes(CType(CDate(Value).Subtract(dt).TotalMilliseconds + 86400 * 1000.0, Double))
                    Case Else
                        Return "UNKNOWN"
                End Select
            End If

            Select Case .fType
                Case enumFieldType.pxfDate To enumFieldType.pxfNumber, enumFieldType.pxfTime To enumFieldType.pxfAutoInc, enumFieldType.pxfLogical
                    Dim IsNull As Boolean = True
                    For i As Integer = 0 To size - 1
                        If ar(i) <> 0 Then IsNull = False : Exit For
                    Next
                    If Not IsNull Then
                        Array.Reverse(ar)
                        If .fType = enumFieldType.pxfNumber Then
                            If IsNeg Then
                                For i As Integer = 0 To size - 1
                                    ar(i) = ar(i) Xor &HFF
                                Next
                            End If
                        End If
                        ar(0) = ar(0) Xor &H80
                    End If
            End Select

            Buffer.BlockCopy(ar, 0, Block.fileData, ptr, size)

            ptr += size
            Return True
        End With
    End Function

    Friend Sub WriteStructure(ByRef bw As IO.BinaryWriter, ByRef struct As Object)
        Dim myType As Type = struct.GetType
        Dim myFields() As System.Reflection.FieldInfo = myType.GetFields
        For Each fld As System.Reflection.FieldInfo In myFields
            'Debug.Print(fld.Name & ": " & fld.GetValue(struct))
            bw.Write(fld.GetValue(struct))
        Next
    End Sub

    Friend Sub ReadStructure(ByRef br As IO.BinaryReader, ByRef struct As Object)
        Try
            br.BaseStream.Seek(0, IO.SeekOrigin.Begin)
            'must copy to ValueType so struct can be updated (structures are value types and are boxed); alternative would be to make it a class
            Dim valStruct As ValueType = struct
            Dim myType As Type = valStruct.GetType
            Dim myFields() As System.Reflection.FieldInfo = myType.GetFields
            For i As Integer = 0 To myFields.Length - 1
                If myFields(i).FieldType Is GetType(Boolean) Then
                    myFields(i).SetValue(valStruct, br.ReadBoolean)
                ElseIf myFields(i).FieldType Is GetType(Byte) Then
                    myFields(i).SetValue(valStruct, br.ReadByte)
                ElseIf myFields(i).FieldType Is GetType(UInt16) Then
                    myFields(i).SetValue(valStruct, br.ReadUInt16)
                ElseIf myFields(i).FieldType Is GetType(UInt32) Then
                    myFields(i).SetValue(valStruct, br.ReadUInt32)
                ElseIf myFields(i).FieldType Is GetType(UInt64) Then
                    myFields(i).SetValue(valStruct, br.ReadUInt64)
                ElseIf myFields(i).FieldType Is GetType(Int16) Then
                    myFields(i).SetValue(valStruct, br.ReadInt16)
                ElseIf myFields(i).FieldType Is GetType(Int32) Then
                    myFields(i).SetValue(valStruct, br.ReadInt32)
                ElseIf myFields(i).FieldType Is GetType(Int64) Then
                    myFields(i).SetValue(valStruct, br.ReadInt64)
                ElseIf myFields(i).FieldType Is GetType(Single) Then
                    myFields(i).SetValue(valStruct, br.ReadSingle)
                ElseIf myFields(i).FieldType Is GetType(Double) Then
                    myFields(i).SetValue(valStruct, br.ReadDouble)
                Else
                    If myFields(i).FieldType.IsEnum Then
                        myFields(i).SetValue(valStruct, br.ReadByte)
                    Else
                        Debug.Assert(False, myFields(i).FieldType.ToString)
                    End If
                End If
            Next
            struct = valStruct
        Catch ex As Exception
            Throw New ApplicationException("Unable to read Paradox file header; the file may be corrupted.", ex)
        End Try
    End Sub

    ''' <summary>
    ''' See if target is found within list of items
    ''' Usage: ItemIn("B","A","B","C") results in True
    ''' </summary>
    Private Function ItemIn(ByVal Target As Object, ByVal ParamArray Items() As Object) As Boolean
        For Each Item As Object In Items
            If Target = Item Then Return True
        Next Item
        Return False
    End Function

    Friend Function NumRecords(ByVal PDoxFilename As String) As Integer
        Dim fs As IO.FileStream = Nothing
        Dim br As IO.BinaryReader = Nothing
        Dim Hdr As PDoxHeader = Nothing
        Dim Hdr2 As PDoxHeader2 = Nothing
        Try
            If Not My.Computer.FileSystem.FileExists(PDoxFilename) Then
                Throw New IO.FileNotFoundException("File not found", PDoxFilename)
                Return 0
            End If
            Try
                fs = New IO.FileStream(PDoxFilename, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.Read, 1024)
            Catch ex As Exception
                Return 0
            End Try
            If fs.Length = 0 Then Throw New ApplicationException(String.Format("Paradox file appears to be corrupt ({0}). Please use table repair utility.", PDoxFilename)) : Return 0
            br = New IO.BinaryReader(fs)
            If GetHeader(br, Hdr, Hdr2) Then Return Hdr.numRecords Else Return 0
        Catch ex As Exception
            Throw
        Finally
            Hdr = Nothing
            Hdr2 = Nothing
            If fs IsNot Nothing Then
                fs.Close()
                fs.Dispose()
                If br IsNot Nothing Then br.Close()
            End If
        End Try
    End Function
End Module

Friend Class clsParadoxField
    Friend fName As String
    Friend fType As enumFieldType
    Friend fSize As Integer

    ''' <summary>
    ''' Initialize Paradox field specification
    ''' </summary>
    ''' <param name="_fName">Field Name</param>
    ''' <param name="_fType">Field Type</param>
    ''' <param name="_fSize">Field Length (Alpha fields only)</param>
    ''' <remarks></remarks>
    Friend Sub New(_fName As String, _fType As enumFieldType, Optional _fSize As Integer = 0)
        fName = _fName
        fType = _fType
        Select Case fType
            Case enumFieldType.pxfAlpha
                fSize = _fSize
            Case enumFieldType.pxfTimestamp
                fSize = 8
            Case enumFieldType.pxfNumber
                fSize = 8
            Case enumFieldType.pxfLogical
                fSize = 1
            Case enumFieldType.pxfLong
                fSize = 4
            Case enumFieldType.pxfShort
                fSize = 2
            Case Else
                Throw New ApplicationException("Invalid file type has been passed to clsParadoxField!")
        End Select
    End Sub

End Class
